#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
	Seagull - The Pyhon Pages engine
	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	This class is the main entry point for running the engine,
	both in CGI or StandAlone mode. 
"""

help = \
"""
Python Pages webserver

Usage: server.py [-r page | -render page] [-s | --server] [-h | --help]

-r page       | --render page       (Renders a page to stdout)
-s            | --server            (Starts standalone server)
-h            | --help              (Shows this help)
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'
__all__ = ()

import getopt
import os
import socket
import sys
import threading

try:
	import configuration
	configuration.SystemConfig()

except configuration.SeagullConfigVariableNotSetException, e:
	print e
	sys.exit(1)

import WebPage
import utils
import renderer
import renderer_input

################################################################
# Output buffers to capture data from the renderer
################################################################

class CGIOutputBuffer(renderer.OutputBuffer):
	""" Output buffer for CGI Requests """
	def __init__(self):
		renderer.OutputBuffer.__init__(self)

	def _write_headers_body(self, headers, status_code):
		# Protocol defined in CGI environment variable
		if os.environ.has_key('SERVER_PROTOCOL'):
			protocol = os.environ['SERVER_PROTOCOL']
		else:
			protocol = 'HTTP/1.0'

#		sys.__stdout__.write('%s %s %s\r\n' % (
#				protocol, status_code, utils.http_status_codes[status_code] ))

		# CGI headers (also the HTTP status is passed as a header field)
		headers['Status'] = '%s %s' % (str(status_code), utils.http_status_codes[status_code])

		for h in headers:
			sys.__stdout__.write('%s: %s\r\n' % (h, headers[h]))
		sys.__stdout__.write('\r\n')

	def _write_content_body(self, string):
		sys.__stdout__.write(string)


class CommandLineOutputBuffer(renderer.OutputBuffer):
	""" Output buffer for Command Line Requests """
	def __init__(self):
		renderer.OutputBuffer.__init__(self)

	def _write_headers_body(self, headers, status_code):
		for h in headers:
			sys.__stdout__.write('%s: %s\r\n' % (h, headers[h]))
		sys.__stdout__.write('\r\n')

	def _write_content_body(self, string):
		sys.__stdout__.write(string)


class ClosingOutputBuffer(renderer.OutputBuffer):
	""" Output buffer for HTTP/1.0 connections which close after each request """
	def __init__(self, connection, protocol='HTTP/1.0'):
		renderer.OutputBuffer.__init__(self)
		self.__connection = connection # Socket
		self.__protocol = protocol

	def _write_headers_body(self, headers, status_code):
		self.__connection.send('%s %s %s\r\n' % (
				self.__protocol, status_code, utils.http_status_codes[status_code] ))
		headers['Connection'] = 'Close'
		for h in headers:
			self.__connection.send('%s: %s\r\n' % (h, headers[h]))
		self.__connection.send('\r\n')

	def _write_content_body(self, string):
		self.__connection.send(string)


class ChunkedOutputBuffer(renderer.OutputBuffer):
	""" Chunk Buffer for HTTP/1.1 Keep-alive connection """
	def __init__(self, connection):
		renderer.OutputBuffer.__init__(self)
		self.__buffer = [ ] # Buffer output, send in chunks of 1000 bytes
		self.__length = 0 # Length of ''.join(self.__buffer)
		self.__connection = connection # Socket

	def _write_headers_body(self, headers, status_code):
		self.__connection.send('HTTP/1.1 %s %s\r\n' % (
				status_code, utils.http_status_codes[status_code] ))
		headers['Connection'] = 'Keep-Alive'
		headers['Transfer-Encoding'] = 'chunked'
		for h in headers:
			self.__connection.send('%s: %s\r\n' % (h, headers[h]))
		self.__connection.send('\r\n')

	def _write_content_body(self, string):
		while self.__length + len(string) >= 1000:
			# Add last part to buffer
			todo = 1000 - self.__length
			self.__buffer.append(string[0:todo])

			# Send 1000 bytes
			self.__connection.send('%s\r\n' % hex(1000)[2:])
			for b in self.__buffer: self.__connection.send(b)
			self.__connection.send('\r\n')

			# Create a new buffer
			string = string[todo:]
			self.__buffer = [ ]
			self.__length = 0

		if len(string) > 0:
			self.__buffer.append(string)
			self.__length += len(string)
	
	def _flush_body(self):
		if self.__length > 0:
			self.__connection.send('%s\r\n' % hex(self.__length)[2:])
			for b in self.__buffer: self.__connection.send(b)
			self.__connection.send('\r\n')

		self.__buffer = [ ]
		self.__length = 0

	def flush_last_chuck(self):
		# Sent last chunk
		self._flush_body()

		# Finish request with a null length chunk
		self.__connection.send('0\r\n\r\n')

		# Prepare for next reply
		self.__writing_headers = True


################################################################
# Classes for the standalone server
################################################################

class StandaloneServer(object):
	def __init__(self):
		host = ''
		port = configuration.SystemConfig().port

		# Open listening socket
		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		s.bind( (host, port) )
		s.listen(5) # allow 5 simultaneous pending connections 

		sys.__stdout__.write('Listening to port %s\n' % port)

		while True:
			connection, address = s.accept()
			def run(connection, address):
				# The parser will call this function for every incoming request
				def request_callback(method, uri, protocol, headers, post_data=None):
					# Create config instance
					config = renderer_input.StandaloneRenderInput(address[0], method, uri,
							protocol, headers, post_data= post_data)

					sys.__stdout__.write('Requested page: %s\n' % config.requested_page.encode('utf-8'))

					# Handle page request
					if protocol.upper() == 'HTTP/1.0':
						renderer.Renderer(config, ClosingOutputBuffer(connection, 'HTTP/1.0'))
						connection.close()

					elif protocol.upper() == 'HTTP/1.1':
						if ((headers.has_key('Connection') and headers['Connection'].lower() != 'close')
									or not headers.has_key('Connection')):
							buffer = ChunkedOutputBuffer(connection)
							renderer.Renderer(config, buffer)
							buffer.flush_last_chuck()
						else:
							renderer.Renderer(config, ClosingOutputBuffer(connection, 'HTTP/1.1'))
							connection.close()
					else:
						connection.send("HTTP/1.0 505 %s" % utils.http_status_codes[505]);
						connection.send('<html><body><p>HTTP Version not supported')
						connection.send('</p></body></html>')
						connection.close()

				def error_callback():
					connection.send('\r\n\r\n')
					connection.send('<html><body><p>Your client has issued a malformed or illegal ')
					connection.send('request</p></body></html>')
					connection.close()

				# Parse incoming data for this connection
				try:
					IncomingDataParser(connection, request_callback, error_callback)
				except IncomingDataParser.MalformedRequestException:
					print 'Malformed Request'

				connection.close()

			threading.Thread(target=run, args=(connection, address)).start()


class IncomingDataParser(object):

	class MalformedRequestException(Exception):
		pass

	class UnknownMethodException(Exception):
		pass

	def __init__(self, connection, request_callback, error_callback):
		self.__request_callback = request_callback
		self.__error_callback = error_callback
		self.__parse_function = self.__parse_method
		self.__done = False

		self.__reset_members()

		connection.settimeout(8) # seconds

		try:
			while True:
				try:
					new_data = connection.recv(1024) # receive up to 1K bytes

					#sys.__stdout__.write('INPUT <<%s>>\n' % new_data) # Debug code

					# The other side has closed the connection, we won't receive
					# any more requests, but possibly, we still can send data
					if len(new_data) == 0:
						return
					else:
						self.__parse_function(new_data)

					# Nothing to parse anymore
					if self.__done:
						return

				except IncomingDataParser.UnknownMethodException:
					self.__error_callback()
					return

		except socket.timeout:
			return

	def __reset_members(self):
		self.__headers = { }
		self.__method = ''
		self.__uri = ''
		self.__protocol = ''

		self.__header_name = ''
		self.__header_value = ''
		self.__post_data_length = 0
		self.__post_data = [ ]

	def __parse_method(self, new_data):
		""" Parse GET, POST, HEAD, ... """
		i = 0
		for char in new_data:
			i += 1

			if char == '\n':
				raise IncomingDataParser.MalformedRequestException()

			elif char.isspace() and len(self.__method):
				self.__parse_function = self.__parse_uri
				self.__parse_function(new_data[i:])
				return
			else:
				self.__method += char

	def __parse_uri(self, new_data):
		""" Parse the URI """
		i = 0
		for char in new_data:
			i += 1

			if char == '\n':
				if len(self.__uri):
					self.__parse_function = self.__parse_headers_name
					self.__parse_function(new_data[i:])
					return
				else:
					raise IncomingDataParser.MalformedRequestException()
			elif char.isspace() and len(self.__uri):
				self.__parse_function = self.__parse_protocol
				self.__parse_function(new_data[i:])
				return
			else:
				self.__uri += char
			
	def __parse_protocol(self, new_data):
		""" Protocol: HTTP/1.0, HTTP/1.1 """
		i = 0
		for char in new_data:
			i += 1

			if char == '\n':
				self.__parse_function = self.__parse_header_name
				self.__parse_function(new_data[i:])
				return
			elif char.isspace():
				pass
			else:
				self.__protocol += char

	def __parse_header_name(self, new_data):
		""" Parse headers' name part """
		i = 0
		for char in new_data:
			i += 1

			# Ignore carriage returns
			if char == '\r':
				pass

			# Double \n, end of request headers
			elif char == '\n' and self.__header_name == '':
				if self.__method == 'POST':
					if (self.__headers.has_key('Content-Type') and self.__headers.has_key('Content-Length') and
								self.__headers['Content-Type'] == 'application/x-www-form-urlencoded'):
						self.__post_data_length = int(self.__headers['Content-Length'])

						# Parse body
						self.__parse_function = self.__parse_body
						self.__parse_function(new_data[i:])
						return
					else:
						self.__done = True # Because body data may follow. We don't understand this yet
						# TODO maybe fix this
						
				elif self.__method == 'GET':
					# Request finished, now call handle callback
					self.__request_callback(self.__method, self.__uri, self.__protocol, self.__headers)

					if (self.__protocol.upper() == 'HTTP/1.1' and (
							not self.__headers.has_key('Connection') or
							(self.__headers.has_key('Connection') and
								self.__headers['Connection'].lower() == 'keep-alive'))):
						# Get ready for a new request
						self.__reset_members()

						self.__parse_function = self.__parse_method
						self.__parse_function(new_data[i:])
						return
					else:
						# Stop parsing requests in this connection
						self.__done = True

				else:
					raise IncomingDataParser.UnknownMethodException()

			# Color, delimiter for header key and value
			elif char == ':':
				self.__parse_function = self.__parse_header_value
				self.__parse_function(new_data[i:])
				return

			# Header name
			else:
				self.__header_name += char


	def __parse_header_value(self, new_data):
		""" Parse headers' value part """
		i = 0
		for char in new_data:
			i += 1

			# \n, start of new header
			if char == '\n':
				# Remember last header
				self.__headers[self.__header_name.strip()] = self.__header_value.strip()

				# Get ready for parsing the next one
				self.__header_name = ''
				self.__header_value = ''

				self.__parse_function = self.__parse_header_name
				self.__parse_function(new_data[i:])
				return

			# Ignore carriage returns
			elif char == '\r':
				pass

			# Header value
			else:
				self.__header_value += char

	def __parse_body(self, new_data):
		""" Parse POST body content """

		if self.__post_data_length == 0:
			self.__request_callback(self.__method, self.__uri, self.__protocol,
					self.__headers, post_data= ''.join(self.__post_data))
			# Get ready for a new request
			self.__reset_members()

			self.__parse_function = self.__parse_method
			self.__parse_function(new_data)
			return
		else:
			if len(new_data) < self.__post_data_length:
				self.__post_data.append(new_data)
				self.__post_data_length -= len(new_data)
			else:
				self.__post_data.append(new_data[0:self.__post_data_length])
				read = self.__post_data_length
				self.__post_data_length = 0
				self.__parse_function(new_data[read:])
		

################################################################
# Main
################################################################

if __name__ == '__main__':
	# Parse comand line options
	opts, args = getopt.gnu_getopt(sys.argv[1:], 'hs:r:', ['help', 'server', 'render=', 'cgi', 'public-directory=', 'pythonpath='])

	settings = {
		'help': False,
		'server': False,
		'render': None,
		'cgi': False,
		'public-directory': '',
		'pythonpath': '',
	}

	for option in opts:
		if option[0] in ('--help', '-h'):
			settings['help'] = True

		elif option[0] in ('--server', '-s'):
			settings['server'] = True

		elif option[0] in ('--render', '-r'):
			settings['render'] = option[1]

		elif option[0] in ('--cgi',):
			settings['cgi'] = True

		elif option[0] in ('--public-directory',):
			settings['public-directory'] = option[1]

		elif option[0] in ('--pythonpath',):
			settings['pythonpath'] = option[1]

	# Show help
	if settings['help']:
		print help
		sys.exit()
	
	# PythonPath
	for p in settings['pythonpath'].split(';'):
		sys.path.append(p)

	# Execute requested action
	if settings['render']:
		renderer.Renderer(renderer_input.SimpleRenderInput(settings['render'], { }), CommandLineOutputBuffer())

	elif settings['server']:
		try:
			StandaloneServer()
		except KeyboardInterrupt, e:
			sys.__stdout__.write('Received keyboard interrupt, stopping server...\n')
	
	elif settings['cgi']:
		# Note that CGI passes the Query string through environment variables like
		# REDIRECT_URL and QUERY_STRING
		
		# Rewrite public CGI url
		if len(settings['public-directory']):
			redirect_url = os.environ['REDIRECT_URL'] 
			remove = settings['public-directory']

			if redirect_url[0:len(remove)] == remove:
				redirect_url = redirect_url[len(remove):]
				os.environ['REDIRECT_URL'] = redirect_url

		# Render page
		renderer.Renderer(renderer_input.CGIRenderInput(), CGIOutputBuffer())

	else:
		print 'Invalid syntax'
		print help
